# eventcal 0.45
# Python calendar class that handles events and outputs to HTML
# Licensed under the MIT License, full-text included in COPYING
# (C) Copyright 2007, 2008, 2009 Rudolf Olah

import datetime
import gettext
import re
from os.path import abspath, dirname
from os.path import join as path_join
from xml.etree.ElementTree import TreeBuilder, tostring

__author__ = 'Rudolf Olah'
__version__ = '0.45'
__email__ = 'omouse@gmail.com'

# Number of days per month copied from calendar.py
mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

language = None # the language that eventcal will output in
mondayToSunday = True # when False, the calendar prints out as Sunday-Saturday

def set_language(lang='en', locale_dir=False):
  global dayNames, monthNames, language
  if locale_dir: # Local locale search
    gettext.translation('eventcal', abspath(path_join(abspath(dirname(__file__)), 'locale')),
                        languages=[lang]).install()
  else:  # System-wide locale search
    gettext.translation('eventcal', languages=[lang]).install()
  language = lang
  dayNames = (_('Monday').decode('utf-8'), _('Tuesday').decode('utf-8'), _('Wednesday').decode('utf-8'),
              _('Thursday').decode('utf-8'), _('Friday').decode('utf-8'), _('Saturday').decode('utf-8'),
              _('Sunday').decode('utf-8'))
  monthNames = (_('January').decode('utf-8'), _('February').decode('utf-8'), _('March').decode('utf-8'),
                _('April').decode('utf-8'), _('May').decode('utf-8'), _('June').decode('utf-8'),
                _('July').decode('utf-8'), _('August').decode('utf-8'),
                _('September').decode('utf-8'), _('October').decode('utf-8'), _('November').decode('utf-8'),
                _('December').decode('utf-8'))

class Event:
  def __init__(self, message, start, length, id=-1):
    """Creates a new event with a message, start, length and id.

    The message is a string.

    The start is a datetime.

    The length is a timedelta, telling us how long the event will last
    for.

    The id is an extra bit of information that can be used to identify
    individual events."""
    self.start = start
    self.length = length
    self.message = message
    self.id = id

  # Property definitions
  def getDay(self):
    return self.start.day

  def getMonth(self):
    return self.start.month

  def getYear(self):
    return self.start.year

  def getHour(self):
    return self.start.hour

  def getDayName(self):
    global dayNames, language
    if language is None:
      raise AttributeError('The language that Eventcal outputs has not been set. Use set_language() to adjust it')
    return dayNames[self.start.weekday()]

  day = property(getDay)
  month = property(getMonth)
  year = property(getYear)
  hour = property(getHour)
  day_name = property(getDayName)
  dayName = property(getDayName)

  def __cmp__(self, other):
    """Comparison method. Returns the difference between the other's
    date and self's date"""
    return (other.year - self.year) + (other.month - self.month) + (other.day - self.day)

  # String representations
  def strtime(self):
    return '%s to %s' % (self.start.strftime('%I:%M%p'),
                         (self.start+self.length).strftime('%I:%M%p'))

  def __repr__(self):
    return '[%i/%i/%i] %s - %s' % (self.day, self.month, self.year,
                                   self.strtime(), self.message)

class Calendar:
  def __init__(self):
    """Creates a new calendar with no events.

    events is a list of Event objects.

    busyDayFormatter is a function that accepts a TreeBuilder and an
    Event. It is used to display information about an event."""
    self.events = []
    self.busyDayFormatter = self.__default_busyDayFormatter

  def add(self, event):
    self.events.append(event)

  def __default_busyDayFormatter(self, tree, anEvent):
    """Builds a day element on an ElementTree builder. Uses information
    from the current event being processed."""
    tree.start('div', {'class': 'daynumber'})
    tree.data(str(anEvent.day))
    tree.end('div')
    tree.start('div', {})
    tree.data(anEvent.message)
    tree.end('div')

  def __cell(self, tree, cellType=u'free', s=u''):
    """Creates a table cell in the element tree with the 'class' set to
    'cellType' with content from the string s"""
    tree.start('td', {'class': cellType})
    tree.data(s)
    tree.end('td')

  def __generate_day_name(self, tree, dayNameIndex, smallify):
    """Using a TreeBuilder tree, generates an HTML table cell with the
    name of the day found in the global variable dayNames."""
    tree.start('td', {})
    if smallify:
      tree.data(dayNames[dayNameIndex][:3])
    else:
      tree.data(dayNames[dayNameIndex])
    tree.end('td')

  def __generate_day_header(self, tree, smallify, left_column_padding=0):
    """Using a TreeBuilder tree, generates an HTML table row with 7
    cells for each day. If smallify is set to True, then the day
    names are cut off after 3 characters. Otherwise, the full name of
    the day is generated. If the global/module variable mondayToSunday
    is set to True, the first day of the week is Monday, otherwise,
    the first day of the week is Sunday."""
    global mondayToSunday, dayNames
    tree.start('tr', {'class': 'daysofweek'})

    for i in xrange(left_column_padding):
      tree.start('td', {})
      tree.end('td')

    if mondayToSunday:
      maxDayIndex = 7 # go in order from 0 to 6, monday to sunday
    else:
      maxDayIndex = 6 # go from 0 to 5, monday to saturday
      # generate sunday first
      self.__generate_day_name(tree, 6, smallify)
    for i in xrange(maxDayIndex):
      self.__generate_day_name(tree, i, smallify)
    tree.end('tr')

  def __week_padding(self, year, month, day=1):
    """Calculates the padding needed for this week. This really only is
    needed for the beginning of the month."""
    dayOfWeek = datetime.date(year, month, day).weekday()
    if mondayToSunday:
      return 1 - dayOfWeek
    else: # sunday to saturday
      if dayOfWeek == 6: # month starts on a Sunday
        return dayOfWeek - 5
      else: # month starts on some other day
        return dayOfWeek - 6

  def eventlist(self):
    """Generates an unordered list with all events listed."""
    global monthNames
    tree = TreeBuilder()
    tree.start('ul', {})
    for event in self.events:
      tree.start('li', {})
      tree.data(event.start.strftime('%A %B %d, %Y'))
      tree.start('br', {})
      tree.end('br')
      tree.data('%s: %s' % (event.length, event.message))
      tree.end('li')
    tree.end('ul')
    return tostring(tree.close())

  def compact_dayview(self, aDate):
    """Generates a two-column table and compacts hours. aDate is the date
    of the day that you want to generate this table for. The type is a
    datetime.date."""
    global dayNames, monthNames
    tree = TreeBuilder()
    tree.start('table', {'class': 'calendar'})
    tree.start('tr', {'class': 'dateheader'})
    tree.start('td', {'colspan': '2'})
    tree.data(u'%s %s %i, %i' % (dayNames[aDate.weekday()],
                                 monthNames[aDate.month], aDate.day, aDate.year))
    tree.end('td')
    tree.end('tr')

    daysEvents = []
    for event in self.events:
      if event.start.day == aDate.day and event.start.month == aDate.month \
            and event.start.year == aDate.year:
        daysEvents.append(event)
    daysEvents.sort()

    freehour = 0
    for event in daysEvents:
      tree.start('tr', {})
      tree.start('td', {'class': 'hour'})
      tree.data(u'%i - %i' % (freehour, freehour + event.hour))
      tree.end('td')
      self.__cell(tree, 'free')
      tree.end('tr')
      tree.start('tr', {})
      tree.start('td', {'class': 'hour'})
      tree.data(u'%i - %i' % (event.hour, (event.start + event.length).hour))
      tree.end('td')
      self.__cell(tree, 'busy', event.message)
      tree.end('tr')
      tree.start('tr', {})
      tree.start('td', {'class': 'hour'})
      if len(daysEvents) > 0:
        last = daysEvents[-1]
        tree.data(u'%i - %i' % ((last.start + last.length).hour,
                                24 - (last.start - last.length).hour))
      else:
        tree.data(u'Free all day')
      tree.end('td')
      self.__cell(tree, 'free')
      tree.end('tr')
    tree.end('table')
    return tostring(tree.close())

  def dayview(self, day, month=1, year=2007, smallify=False):
    """Generates a two-column table for the specific day with one column
    holding the hours and the other holding any events"""
    global dayNames, monthNames
    tree = TreeBuilder()
    tree.start('table', {'class': 'calendar'})
    tree.start('tr', {'class': 'dateheader'})
    tree.start('td', {'colspan': 2})
    tree.data('%s %s %i, %i' % (dayNames[datetime.date(year, month, day).weekday()],
                                monthNames[month], day, year))
    tree.end('td')
    tree.end('tr')

    daysEvents = []
    for event in self.events:
      if event.start.day == day and event.start.month == month and event.start.year == year:
        daysEvents.append(event)
    daysEvents.sort()

    for i in range(0, 24):
      tree.start('tr', {})
      tree.start('td', {'class': 'hour'})
      tree.data('%i:00' % i)
      tree.end('td')

      hasEvent = False
      for event in daysEvents:
        if i == event.start:
          hasEvent = True
          self.__cell(tree, 'busy', event.message)
          break
        elif i > event.start.hour and i < (event.start + event.length).hour:
          hasEvent = True
          self.__cell(tree, 'busy')
          break
      if not hasEvent:
        self.__cell(tree, 'free')
    tree.end('table')
    return tostring(tree.close()).encode('utf-8')

  def weekview(self, day, month=1, year=2007, smallify=False):
    global dayNames, monthNames
    tree = TreeBuilder()
    tree.start('table', {'class': 'calendar'})
    tree.start('tr', {'class': 'dateheader'})
    tree.start('td', {'colspan': '8'})
    tree.data('%s %i' % (monthNames[month - 1], year))
    tree.end('td')
    tree.end('tr')

    self.__generate_day_header(tree, smallify, 1)
    tree.start('tr', {})
    tree.start('td', {})
    tree.end('td')
    if mondayToSunday:
      dayOfWeek = datetime.date(year, month, day).weekday()
      maxDayOfWeek = 7
    else:
      dayOfWeek = datetime.date(year, month, day).weekday() - 1
      maxDayOfWeek = 6
    if day == 1: # generate padding for the first week of the month
      for i in xrange(dayOfWeek):
        tree.start('td', {})
        tree.end('td')
    for i in xrange(dayOfWeek, maxDayOfWeek):
      tree.start('td', {})
      if (day + i) <= mdays[month]:
        tree.data('%i' % (day + i))
      tree.end('td')
    tree.end('tr')

    hour = 0
    events = []
    try:
      events = [x for x in self.events
                if (x.start >= datetime.datetime(year, month, day))]
    except ValueError:
      pass

    while hour < 25:
      tree.start('tr', {})
      tree.start('td', {'class': 'hour'})
      tree.data('%i:00' % hour)
      tree.end('td')
      col = 0
      while col < 7:
        if day + col < 1 or (day + col) > mdays[month]:
          # not a valid day in the month
          self.__cell(tree, 'noday')
        else:
          hasEvent = False
          for event in events:
            if event.month == month and event.day == (day + col):
              if event.hour == hour: # start of event
                hasEvent = True
                self.__cell(tree, 'busy', event.message)
              elif hour > event.hour and hour < (event.start + event.length).hour:
                # event continuation
                hasEvent = True
                self.__cell(tree, 'busy')
          if not hasEvent:
            self.__cell(tree, 'free')
        col += 1
      tree.end('tr')
      hour += 1

    tree.end('table')
    return tostring(tree.close()).encode('utf-8')

  def monthview(self, month=1, year=2007, smallify=False):
    global mondayToSunday, dayNames, monthNames
    tree = TreeBuilder()
    tree.start('table', {'class': 'calendar'})
    tree.start('tr', {'class': 'dateheader'})
    tree.start('td', {'colspan': '7'})
    tree.data(u'%s %i' % (_(monthNames[month - 1]), year))
    tree.end('td')
    tree.end('tr')
    self.__generate_day_header(tree, smallify)

    row = 0
    day = self.__week_padding(year, month)
    while row < 5: # each week in the month
      tree.start('tr', {})
      col = 0
      while col < 7: # each day in the month
        if day <= 0 or day > mdays[month]: # not a valid day in the month
          self.__cell(tree, 'noday')
        else:
          daysEvents = [x for x in self.events if (x.start.day == day)
                        and (x.start.month == month)
                        and (x.start.year == year)]
          if daysEvents: # there are some events
            tree.start('td', {'class': 'busy'})
            for event in daysEvents:
              tree.start('br', {})
              tree.end('br')
              self.busyDayFormatter(tree, event)
            tree.end('td')
          else: # there are no events
            tree.start('td', {'class': 'free'})
            tree.start('div', {'class': 'daynumber'})
            tree.data('%d' % day)
            tree.end('div')
            tree.end('td')
        day += 1
        col += 1
      tree.end('tr')
      row += 1
    tree.end('table')
    return tostring(tree.close()).encode('utf-8')

def today(compact=False):
  cal = Calendar()
  x = datetime.datetime.today()
  cal.add(Event("Today", x, datetime.timedelta(hours=1)))
  if compact:
    return cal.monthview(x.month, x.year, True)
  return cal.monthview(x.month, x.year, False)
